package reports;

import java.util.*;

import model.entity.Item;
import model.entity.Product;
import model.entity.container.ProductGroup;
import model.entity.container.StorageUnit;
import model.management.ItemManager;
import model.management.ProductManager;
import reports.builders.IBuilder;
import reports.display.ReportTable;
import reports.display.ReportTableRow;
import reports.visitor.IVisitor;

import common.util.DateUtils;

public class ProductStatisticsReport extends Report
{
  private static final String[] COLUMNS = new String[]
                                        {
      "Description       ",
      "Barcode           ",
      "Size              ",
      "3-Month Supply",
      "Supply:\nCurrent/Average",
      "Supply:\nMin/Max",
      "Supply:\nUsed/Added",
      "Shelf Life (months)",
      "Age at Use:\nAverage/Max",
      "Current Age:\nAverage/Max"
                                        };
  private static final String   TITLE   = "Product Statistics (%d Month Period)";

  private static final String getTitle(int months)
  {
    return String.format(TITLE, months);
  }

  private IVisitor                visitor;

  private ReportTable             table;
  private Map<Product, Set<Item>> data;

  private Date                    endDate;
  private Date                    startDate;

  private Map<Product, Integer>   currentSupply     = new HashMap<Product, Integer>();
  private Map<Product, Float>     averageSupply     = new HashMap<Product, Float>();
  private Map<Product, Integer>   minSupply         = new HashMap<Product, Integer>();
  private Map<Product, Integer>   maxSupply         = new HashMap<Product, Integer>();
  private Map<Product, Integer>   used              = new HashMap<Product, Integer>();
  private Map<Product, Integer>   added             = new HashMap<Product, Integer>();
  private Map<Product, Float>     averageUsedAge    = new HashMap<Product, Float>();
  private Map<Product, Integer>   maxUsedAge        = new HashMap<Product, Integer>();
  private Map<Product, Float>     averageCurrentAge = new HashMap<Product, Float>();
  private Map<Product, Integer>   maxCurrentAge     = new HashMap<Product, Integer>();

  public ProductStatisticsReport(int months)
  {
    super(getTitle(months));

    table = new ReportTable(null, COLUMNS);

    endDate = DateUtils.currentDate();
    startDate = DateUtils.addMonthsToDate(endDate, -months);

    Comparator<Product> comparator = getProductComparator();
    data = new TreeMap<Product, Set<Item>>(comparator);

    visitor = new ProductStatisticsReportVisitor();
  }

  private Comparator<Product> getProductComparator()
  {
    return new Comparator<Product>()
    {
      @Override
      public int compare(Product o1, Product o2)
      {
        int result = o1.getDescription().compareToIgnoreCase(o2.getDescription());
        if (result == 0)
          result = o1.getBarcode().compareTo(o2.getBarcode());

        return result;
      }
    };
  }

  public Iterator<Product> getProducts()
  {
    return Collections.unmodifiableSet(data.keySet()).iterator();
  }

  private int valueOf(Integer i)
  {
    if (i != null)
      return i;
    else
      return 0;
  }

  private float valueOf(Float f)
  {
    if (f != null)
      return f;
    else
      return 0f;
  }

  public int getCurrentSupply(Product product)
  {
    return valueOf(currentSupply.get(product));
  }

  public float getAverageSupply(Product product)
  {
    return valueOf(averageSupply.get(product));
  }

  public int getMinSupply(Product product)
  {
    return valueOf(minSupply.get(product));
  }

  public int getMaxSupply(Product product)
  {
    return valueOf(maxSupply.get(product));
  }

  public int getQuantityUsed(Product product)
  {
    return valueOf(used.get(product));
  }

  public int getQuantityAdded(Product product)
  {
    return valueOf(added.get(product));
  }

  public float getAverageUsedAge(Product product)
  {
    return valueOf(averageUsedAge.get(product));
  }

  public int getMaxUsedAge(Product product)
  {
    return valueOf(maxUsedAge.get(product));
  }

  public float getAverageCurrentAge(Product product)
  {
    return valueOf(averageCurrentAge.get(product));
  }

  public int getMaxCurrentAge(Product product)
  {
    return valueOf(maxCurrentAge.get(product));
  }

  @Override
  protected void buildReport(IBuilder<?> builder)
  {
    builder.addTitle(getTitle());
    builder.startTable(table.getColumnCount());
    builder.addTableHeader(table.getColumnTitles());
    Iterator<ReportTableRow> itr = table.getRows();
    while (itr.hasNext())
      builder.addRow(itr.next().getCells());
    builder.endTable();
  }

  // ///////////////////////////////
  // Report generation methods
  // ///////////////////////////////

  @Override
  protected void visitData() throws Exception
  {
    ProductManager.getInstance().preOrderAccept(visitor);
    ItemManager.getInstance().preOrderAccept(visitor);
  }

  @Override
  protected void interpretData() throws Exception
  {
    for (Map.Entry<Product, Set<Item>> entry : data.entrySet())
    {
      Product product = entry.getKey();
      Set<Item> items = entry.getValue();
      ReportTableRow row = table.createRow();

      row.setCell(0, product.getDescription());
      row.setCell(1, product.getBarcode().getCode());
      row.setCell(2, product.getSize());

      if (product.getThreeMonthSupply() > 0)
        row.setCell(3, product.getThreeMonthSupply());
      else
        row.setCell(3, "Unspecified");

      row.setCell(4, getCurrentAverageSupply(items));
      row.setCell(5, getMinMaxSupply(items));
      row.setCell(6, getUsedAddedSupply(items));

      if (product.getShelfLife() > 0)
        row.setCell(7, product.getShelfLife());
      else
        row.setCell(7, "Unspecified");

      row.setCell(8, getUsedAvgMaxAge(items));
      row.setCell(9, getCurrentAvgMaxAge(items));

      table.addRow(row);
    }

    // System.out.println(table.prettyPrint());
  }

  // ///////////////////////////////
  // Column data methods
  // ///////////////////////////////

  private String getCurrentAverageSupply(Set<Item> items)
  {
    int current = getCurrentSupply(items);
    float avg = getAverageSupply(items);

    Product product = getProduct(items);
    if (product != null)
    {
      currentSupply.put(product, current);
      averageSupply.put(product, avg);
    }

    return String.format("%d / %1.1f", current, avg);
  }

  private int getCurrentSupply(Set<Item> items)
  {
    int itemsInSystem = 0;

    for (Item item : items)
    {
      if (isInSystem(item))
        itemsInSystem++;
    }

    return itemsInSystem;
  }

  private float getAverageSupply(Set<Item> items)
  {
    int days = DateUtils.getDifferenceInDays(startDate, endDate);
    int total = 0;

    for (int i = 0; i <= days; i++)
    {
      Date date = DateUtils.addDaysToDate(startDate, i);
      total += getSupply(items, date);
    }

    return total / (days + 1f);
  }

  private String getMinMaxSupply(Set<Item> items)
  {
    int min = Short.MAX_VALUE;
    int max = 0;

    int days = DateUtils.getDifferenceInDays(startDate, endDate);
    for (int i = 0; i <= days; i++)
    {
      Date date = DateUtils.addDaysToDate(startDate, i);
      int supply = getSupply(items, date);
      if (supply < min)
        min = supply;
      if (supply > max)
        max = supply;
    }

    Product product = getProduct(items);
    if (product != null)
    {
      minSupply.put(product, min);
      maxSupply.put(product, max);
    }

    return String.format("%d / %d", min, max);
  }

  private String getUsedAddedSupply(Set<Item> items)
  {
    int added = 0, used = 0;

    for (Item item : items)
    {
      if (wasAddedDuringReportPeriod(item))
        added++;
      if (wasRemovedDuringReportPeriod(item))
        used++;
    }

    Product product = getProduct(items);
    if (product != null)
    {
      this.used.put(product, used);
      this.added.put(product, added);
    }

    return String.format("%d / %d", used, added);
  }

  private String getUsedAvgMaxAge(Set<Item> items)
  {
    int max = 0, total = 0, itemsUsed = 0;

    for (Item item : items)
    {
      if (wasRemovedDuringReportPeriod(item))
      {
        int age = getAge(item);
        if (age > max)
          max = age;

        total += age;
        itemsUsed++;
      }
    }

    float avg = (total * 1f) / itemsUsed;

    Product product = getProduct(items);
    if (product != null)
    {
      averageUsedAge.put(product, avg);
      maxUsedAge.put(product, max);
    }

    if (Float.isNaN(avg))
      return "No items used during report period";
    else
      return String.format("%1.1f days /\n%d days", avg, max);
  }

  private String getCurrentAvgMaxAge(Set<Item> items)
  {
    int max = 0, total = 0, itemsInSystem = 0;

    for (Item item : items)
    {
      if (isInSystem(item))
      {
        int days = getAge(item);
        if (days > max)
          max = days;

        total += days;
        itemsInSystem++;
      }
    }

    float avg = (total * 1f) / itemsInSystem;

    Product product = getProduct(items);
    if (product != null)
    {
      averageCurrentAge.put(product, avg);
      maxCurrentAge.put(product, max);
    }

    return String.format("%1.1f days /\n%d days", avg, max);
  }

  // ///////////////////////////////
  // Utilities methods
  // ///////////////////////////////

  private boolean wasAddedDuringReportPeriod(Item item)
  {
    Date entryDate = item.getEntryDate();
    // entryDate guaranteed to be non-null

    boolean addedAfterStart = startDate.compareTo(entryDate) <= 0;
    boolean addedBeforeEnd = endDate.compareTo(entryDate) >= 0;

    return addedAfterStart && addedBeforeEnd;
  }

  private boolean wasRemovedDuringReportPeriod(Item item)
  {
    Date exitDate = item.getExitTime();
    if (exitDate == null)
      return false;

    exitDate = DateUtils.removeTimeFromDate(exitDate);

    boolean removedAfterStart = startDate.compareTo(exitDate) <= 0;
    boolean removedBeforeEnd = endDate.compareTo(exitDate) >= 0;

    return removedAfterStart && removedBeforeEnd;
  }

  private int getSupply(Set<Item> items, Date date)
  {
    int supply = 0;

    for (Item item : items)
    {
      if (wasInSupplyOn(item, date))
        supply++;
    }

    return supply;
  }

  private boolean wasInSupplyOn(Item item, Date date)
  {
    Date entryDate = item.getEntryDate();

    // test to see if item was added on or before the given date
    boolean inSupply = entryDate.equals(date) || entryDate.before(date);

    Date exitTime = item.getExitTime();
    if (exitTime != null)
    {
      exitTime = DateUtils.removeTimeFromDate(exitTime);
      // test to see if item was removed after the given date
      inSupply &= exitTime.after(date);
    }

    return inSupply;
  }

  private boolean isRemoved(Item item)
  {
    return item.getExitTime() != null;
  }

  private boolean isInSystem(Item item)
  {
    if (!isRemoved(item))
    {
      Date entryDate = item.getEntryDate();
      if (entryDate.compareTo(endDate) > 0)
        return false;
    }

    Date exitTime = item.getExitTime();
    if (exitTime != null)
    {
      exitTime = DateUtils.removeTimeFromDate(exitTime);
      return endDate.compareTo(exitTime) < 0;
    }
    else
      return true;
  }

  private int getAge(Item item)
  {
    Date end = item.getExitTime();
    if (end == null)
      end = DateUtils.currentDate();
    else
      end = DateUtils.removeTimeFromDate(end);

    Date start = item.getEntryDate();

    return DateUtils.getDifferenceInDays(start, end);
  }

  private Product getProduct(Set<Item> items)
  {
    try
    {
      return items.iterator().next().getProduct();
    }
    catch (NoSuchElementException e)
    {
      return null;
    }
  }

  // ///////////////////////////////
  // Visitor
  // ///////////////////////////////

  private class ProductStatisticsReportVisitor implements IVisitor
  {
    @Override
    public void visit(ProductGroup productgroup)
    {
      // do nothing
    }

    @Override
    public void visit(StorageUnit storageunit)
    {
      // do nothing
    }

    @Override
    public void visit(Item item)
    {
      Set<Item> items = data.get(item.getProduct());
      if (items == null)
      {
        items = new HashSet<Item>();
        data.put(item.getProduct(), items);
      }

      items.add(item);
    }

    @Override
    public void visit(Product product)
    {
      if (data.get(product) == null)
        data.put(product, new HashSet<Item>());
    }
  }
}
